package epsofts.Neutron;

import java.util.HashSet;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.Map.Entry;

import epsofts.utils.EPFctPart;

/**
 * Modélise un plateau de jeu (25 cases étant soit vide, soit occupée par une
 * bille rouge, bleu, ou par le neutron.
 * 
 * @version 1.0
 * @created 14-sept.-2006 22:48:10
 */
public class Plateau implements Cloneable
{
	/** Classe d'exception de Plateau */
	public static class PlateauException extends Exception
	{
		static final long serialVersionUID = 1111;
		
		/**
		 * Constructeur de la classe d'exception PlateauException
		 */
		public PlateauException(String sMessage)
		{
			super(sMessage);
		}
	};

	/**
	 * Cases du plateau 5x5.
	 */
	private final eCase[]	m_Cases;//	= new eCase[25];

	/**
	 * Le plateau est-il un premier coup ? Si oui, on ne bouge pas le neutron ce
	 * coup- ci.
	 */
	private final boolean	m_bPremierCoup;// = true;
	
	/**
	 * Indique la génération du plateau (combien de tour ont été nécéssaires pour y venir.
	 */
	private final short m_Generation;

	/**
	 * Prend la couleur du joueur dont c'est le tour de jouer sur ce plateau.
	 */
	private final eCase	m_Tour;
	
	/**
	 * Coup précédent ayant conduit à ce plateau.
	 */
	private final Coup m_dernierCoup;

	public Plateau(Plateau plateau)
	{
		m_Cases = plateau.m_Cases.clone();
		m_Tour = plateau.m_Tour;
		m_bPremierCoup = plateau.m_bPremierCoup;
		m_Generation = plateau.m_Generation;
		m_dernierCoup = plateau.m_dernierCoup;
		
		verifCasesNull();
	}
	
	public Plateau(eCase[] i_cases, eCase i_tour, short i_Generation, Coup i_dernierCoup)
	{
		m_Cases = i_cases;		
		m_Tour = i_tour;
		m_bPremierCoup = false;
		m_Generation = i_Generation;
		
		m_dernierCoup = i_dernierCoup;
		
		verifCasesNull();
	}
	
	public Plateau()
	{
		m_Cases = new eCase[25];
		short i;
		verifCasesNull();
		for(i = 0; i < 5; ++i )
		{
			m_Cases[i] = eCase.ROUGE;
			m_Cases[20 + i] = eCase.BLEUE;			
		}
		m_Cases[12] = eCase.NEUTRON;
		
		m_bPremierCoup = true;
		m_Generation = 0;
		m_Tour = eCase.BLEUE;
		
		m_dernierCoup = null;		
	}
	
	private void verifCasesNull()
	{
		for(short i=0; i < 25; ++i)
		{
			if (m_Cases[i] == null) m_Cases[i] = eCase.VIDE;
		}
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object i_obj)
	{
		if (i_obj instanceof String)
		{
			return false;
		}
		return (toString().compareTo(i_obj.toString()) == 0);
	}
	
	/* (non-Javadoc)
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode()
	{
		return toString().hashCode();
	}
	
	/**
	 * Cherche où se trouve le neutron sur le plateau.
	 * @return Position du neutron.
	 */
	private static short trouverNeutron(eCase[] cases)
	{
		for(short i=0; i<cases.length; ++i)
		{
			if (cases[i] == eCase.NEUTRON)
			{
				return i;
			}
		}
		
		throw new RuntimeException("Plateau corrompu : Neutron introuvable.");
	}
	
	public Coup getDernierCoup()
	{
		return m_dernierCoup;
	}
	
	/**
	 * Joue un coup sur le plateau (ne modifie pas le plateau) et retourne le nouveau plateau.
	 * Attention, on peut jouer un coup invalide.
	 * @param i_coup Coup à jouer.
	 * @return Plateau nouveau plateau.
	 */
	public Plateau jouerCoup(Coup i_coup)
	{
		eCase[] nouvellesCases = jouerCoup(m_Cases, i_coup, m_Tour);		
		return new Plateau(nouvellesCases, inverserBille(m_Tour), ((short) (m_Generation+1)), i_coup);
	}
	
	private static eCase[] jouerCoup(eCase[] cases, Coup i_coup, eCase tour)
	{
		eCase[] nouvellesCases = cases.clone();
		
		nouvellesCases[i_coup.getNeutronOrigine()] = eCase.VIDE;
		nouvellesCases[i_coup.getNeutronDestination()] = eCase.NEUTRON;		
		nouvellesCases[i_coup.getBilleOrigine()] = eCase.VIDE;
		nouvellesCases[i_coup.getBilleDestination()] = tour;
		
		return nouvellesCases;				
	}
	
	/**
	 * Déjoue un coup sur la plateau courant (modifie le plateau).
	 * Attention, on peut déjouer un coup invalide, ou déjouer un coup qui n'est pas le dernier joué.
	 * @param i_coup Coup à déjouer.
	 */
	/*
	public void annulerCoup()
	{
		if (m_derniersCoups.size() <= 0) return;
		Coup dernierCoup = m_derniersCoups.remove(m_derniersCoups.size()-1);
		annulerCoup(dernierCoup);
	}
	private synchronized void annulerCoup(final Coup i_coup)
	{
		m_Generation--;
		m_bPremierCoup = (m_Generation == 0);
		m_Tour = inverserBille(m_Tour);
		
		m_Cases[i_coup.getBilleDestination()] = eCase.VIDE;
		m_Cases[i_coup.getBilleOrigine()] = m_Tour;
		m_Cases[i_coup.getNeutronDestination()] = eCase.VIDE;
		m_Cases[i_coup.getNeutronOrigine()] = eCase.NEUTRON;
		
		m_Neutron = trouverNeutron();
	}
	*/

	/**
	 * Teste que le coup proposé soit valide sur ce plateau.
	 */
	public boolean testerValiditeCoup(Coup i_coup)
	{
		return testerValiditeCoup(m_Cases, m_bPremierCoup, m_Tour, i_coup);
	}
	private static boolean testerValiditeCoup(eCase[] cases, final boolean premierCoup, eCase tour, final Coup i_coup)
	{
		// Le coup est invalide ssi :
		
		// Le plateau est terminal.
		// TODO : A voir si on fait un test direct ou pas ici...
		if (testerPlateauTerminal(cases, premierCoup, tour, false) != eCase.VIDE) return false;
		 
		// On ne joue pas le neutron (même au premier coup)
		if (cases[i_coup.getNeutronOrigine()] != eCase.NEUTRON) return false;
		// On joue le neutron sur une case non vide, sauf la sienne au premier coup.
		if ((cases[i_coup.getNeutronDestination()] != eCase.VIDE) && ((cases[i_coup.getNeutronDestination()] != eCase.NEUTRON) || !premierCoup)) return false;
		// On joue une bille de mauvaise couleur (ou case vide).
		if (cases[i_coup.getBilleOrigine()] != tour) return false;
		// On ne joue pas la bille sur une case vide.
		if ((cases[i_coup.getBilleDestination()] != eCase.VIDE) && (i_coup.getBilleDestination() != i_coup.getNeutronOrigine())) return false;
		
		// Le mouvement du neutron est incorrect :
		// C'est le premier coup, et on a bougé le neutron
		if (premierCoup)
		{
			if  (i_coup.getNeutronDestination() != trouverNeutron(cases)) return false;
		}
		// Ce n'est pas le premier coup, et on a bougé incorrectement le neutron
		else if (!testerMouvementBille(cases, premierCoup, i_coup.getNeutronOrigine(), i_coup.getNeutronDestination(), trouverNeutron(cases))) return false;
		
		// Le mouvement de la bille est incorrect.
		if (!testerMouvementBille(cases, premierCoup, i_coup.getBilleOrigine(), i_coup.getBilleDestination(), i_coup.getNeutronDestination())) return false;		
		
		// Autrement, le coup est valide
		return true;
	}
	
	/**
	 * Teste que la position soit jouable pour la bille donnée, en supposant une nouvelle position au neutron.
	 * @param i_billeOrigine position d'origine de la bille.
	 * @param i_billeDestination position destination de la bille.
	 * @param i_nouveau_neutron nouvelle position du neutron.
	 * @return true si la bille peut être jouée à l'endroit donné, false sinon.
	 */
	public boolean testerMouvementBille(final short i_billeOrigine, final short i_billeDestination)
	{
		return testerMouvementBille(i_billeOrigine, i_billeDestination, getNeutron());
	}
	public boolean testerMouvementBille(final short i_billeOrigine, final short i_billeDestination, final short i_nouveau_neutron)
	{
		return testerMouvementBille(m_Cases, m_bPremierCoup, i_billeOrigine, i_billeDestination, i_nouveau_neutron);
	}
	
	private static boolean testerMouvementBille(eCase[] cases, boolean premierCoup, final short i_billeOrigine, final short i_billeDestination, final short i_nouveau_neutron)
	{
		// Faux si la case destination est occupée par une bille bleu ou rouge.
		if ((cases[i_billeDestination] == eCase.BLEUE) || (cases[i_billeDestination] == eCase.ROUGE)) return false;
		// Faux si la case destination est occup�e par le neutron et que la bille d'origine n'est pas le neutron, ou que nous ne somme pas au premier coup.
		if ((i_billeDestination == i_nouveau_neutron) && ((i_billeOrigine != i_nouveau_neutron) || !premierCoup)) return false;
		
		short i, coups_bille[];
		coups_bille = listerCoupsBille(cases, i_billeOrigine, i_nouveau_neutron);
		for(i=7; (i>=0) && (coups_bille[i] != i_billeDestination); --i);
		
		return (i >= 0);
	}
	
	/**
	 * Teste les coups possible de la bille sur le plateau en ayant supposé une nouvelle position du neutron.
	 * @param i_bille Position de la bille à tester
	 * @param i_nouveau_neutron Position supposée du neutron (qui n'as pas encore été joué)
	 */	
	public short[] listerCoupsBille(final short i_bille)
	{
		return listerCoupsBille(i_bille, getNeutron());
	}
	public short[] listerCoupsBille(final short i_bille, final short i_nouveau_neutron)
	{
		return listerCoupsBille(m_Cases, i_bille, i_nouveau_neutron);
	}
	
	private static short[] listerCoupsBille(eCase[] cases, final short i_bille, final short i_nouveau_neutron)
	{
		short dir, dist, posn, posm;
		short[] move = new short[8];
		
		cases = cases.clone();
		cases[trouverNeutron(cases)] = eCase.VIDE;
		cases[i_nouveau_neutron] = eCase.NEUTRON;
		
		for(dir=0; dir < 8; ++dir)
		{
			posn = i_bille;

			for(dist=1; dist<5; ++dist)
			{
				try
				{
					posm = testerDeplacement(i_bille, dist, dir);
				}
				catch(PlateauException e)
				{
					break;
				}
				
				if (cases[posm] == eCase.VIDE)
				{
					posn = posm;
				}
				else
				{
					break;
				}
			}
			move[dir] = posn;
		}
		
		return move;
	}

	/**
	 * Renvoi l'index de la case à la distance et dans la direction donnée par rapport à la position donnée.
	 * @param i_pos 
	 * @param i_dist
	 * @param i_dir
	 * @return index de la case testé si elle existe.
	 * @throws PlateauException en cas de dépassement de bornes. //NOTE: Important !
	 */
	static private short testerDeplacement(final short i_pos, final short i_dist, final short i_dir) throws PlateauException
	{
		short[] coords = i2coord(i_pos);
		short x = coords[0], y = coords[1];
		
        switch(i_dir)
        {
        case 0: y = (short) (y - i_dist);
                break;
        case 1: y = (short) (y - i_dist);
                x = (short) (x + i_dist);
                break;
        case 2: x = (short) (x + i_dist);
                break;
        case 3: x = (short) (x + i_dist);
                y = (short) (y + i_dist);
                break;
        case 4: y = (short) (y + i_dist);
                break;
        case 5: y = (short) (y + i_dist);
                x = (short) (x - i_dist);
                break;
        case 6: x = (short) (x - i_dist);
                break;
        case 7: x = (short) (x - i_dist);
                y = (short) (y - i_dist);
                break;
        }
        
        return coord2i(x, y);
	}
	
	/**
	 * Renvoi les coordonnées [1-5] d'une position [0-24] donnée.
	 * @param i index de la case du plateau.
	 * @return tableau de deux entiers, x et y.
	 */
	static private short[] i2coord(final short i)
	{
		short[] retour = new short[2];
		retour[0] = (short) ((i % 5) + 1);
		retour[1] = (short) ((i / 5) + 1);
		
		return retour;
	}

	/**
	 * Renvoi l'index de la case [0-24] correspondant aux coordonnées données [1-5].
	 * @param x coordonnée en x.
	 * @param y coordonnée en y.
	 * @return index de la case, -1 en cas de dépassement (ne génére pas d'exception).
	 * @throws PlateauException en cas de dépassement de bornes.
	 */
	static private short coord2i(final short x, final short y) throws PlateauException
	{
		if ((x < 1) || (x > 5) || (y < 1) || (y > 5))
		{
			throw new PlateauException(String.format("coord2i(%d, %d) : hors bornes.", x, y));
		}
	    else
	    {
	    	return (short) ((y - 1) * 5 + x - 1);
	    }
	}
	
	private static final Random rnd = new Random();
	private static enum eSituationPlateau {/*BLOCABLE, */VICTOIRE/*, DEFAITE*/};
	public static Entry<Plateau, eCase> genererPlateauQuasiFinal()
	{				
		final eCase[] cases = new eCase[25];
		
		short i;
		for(i = 0; i < 25; ++ i)
		{			
			cases[i] = eCase.VIDE;
		}
		
		short nbRouges = 0, nbBleues = 0;
		
		// Parmis les 9 positions possibles d'un plateau normalisé.
		short x = (short) rnd.nextInt(3);
		short y = (short) rnd.nextInt(3);
		short neutron = (short) (x + 5*(y+1));
		short neutronDefaite = -1;
			
		cases[neutron] = eCase.NEUTRON;
		
		eSituationPlateau situation = eSituationPlateau.values()[rnd.nextInt(eSituationPlateau.values().length)];
		
		short directionChoisie = -1, directionDefaite = -1, distanceDéfaite = -1;
		
		final eCase vainqueur;
		
		switch (situation)
		{
		/*
			case BLOCABLE:
			{
				do
				{
					directionChoisie = (short) rnd.nextInt(8);
					try
					{
						testerDeplacement(neutron, (short) 2, directionChoisie);
					}
					catch(PlateauException e)
					{
						directionChoisie = -1;
					}
				}while(directionChoisie < 0);
				vainqueur = eCase.ROUGE;
				break;
			}
			
			/*
			case DEFAITE:
			{
				while(directionChoisie < 0)
				{
					directionChoisie = (short) rnd.nextInt(8);
					// Direction interdites selon la position du neutron
					if (y == 0 && (directionChoisie <= 1 || directionChoisie >=7)) directionChoisie = -1; // Hautes
					if (x == 0 && (directionChoisie >= 5)) directionChoisie = -1; // Gauche
					if (y == 2 && (directionChoisie >= 3 && directionChoisie <= 5)) directionChoisie = -1; // Bases
				}
				
				while(distanceDéfaite < 0)
				{
					// Distance bornée à 1 selon la direction
					if (x == 1 && (directionChoisie >= 5)) distanceDéfaite = 1; // Gauches
					else if (y == 1 && (directionChoisie != 2 && directionChoisie != 6)) distanceDéfaite = 1; // Hautes ou basses
					// Sans conrainte, borné à 2
					else distanceDéfaite = (short) (rnd.nextInt(2)+1);
				}
				/*				
				1			TirerAuHasard distanceForcée
				o				SI neutron x=1 ET direction gauches : distanceForcée <- 1
				x				SI neutron y=1 ET direction hautes ou basses : distanceForcée <- 1
								
				10			Déduire neutronForcée
				*/
				/*break;
			}
			/*
			neutron, direction, distance, neutronForcé, directionDéfaiteChoisi[] 
			
15			TirerAuHasard le neutron

1			TirerAuHasard direction
x				SI neutron y=0 : !direction hautes
o				SI neutron x=0 : !direction gauches
o				SI neutron y=2 : !direction basses
2			TirerAuHasard distanceForcée
x				SI neutron x=1 ET direction gauches : distanceForcée <- 1
x				SI neutron y=1 ET direction hautes ou basses : distanceForcée <- 1
				
7			Déduire neutronForcée

			Placer les billes de blocage obligatoires (celles n'obstruant aucun chemin de défaite du neutron courant)
				de telle façon qu'elle n'obstrue aucun chemin de défaite du neutronForcée				
			
3,4			Noter les directions de défaite du neutron forcé qui sont obstruables par rouge.
			Choisir au hasard entre "2 chemin de défaite obstruable par rouge" ou "un chemin de défaite non obstruable par rouge"
			TANTQUE choix impossible FAIRE changer une bille rouge en bille bleue;

			Placer le restant de bille en obstruant pas le chemin de forçage,
				Si 1 seul chemin de défaite choisi, s'assurer qu'il reste non-obstruable par rouge.														
			 */			
			
			case VICTOIRE:
			{
				do
				{
					directionChoisie = (short) (rnd.nextInt(3)-1);
					if (directionChoisie < 0) directionChoisie = 7;
				}while(directionChoisie < 0 || !cheminLigneDepart(neutron, directionChoisie));
								
				vainqueur = eCase.ROUGE;
				break;
			}
	
			default:
				throw new RuntimeException("Unknown case "+situation);
		}
		
		short bouchon = -1, bouchonDistance = -1;
		
		short nouvelleBille = -1;
		eCase couleurNouvelleBille = eCase.VIDE;
		
		while(nbRouges < 5 || nbBleues < 5)
		{
			switch (situation)
			{
			/*
				TODO: Refaire l'algo, code faux (on met le neutron dans le trou..)
				case BLOCABLE:
				{
					if (calculerDegreLiberte(cases, neutron) > 1)
					{
						do
						{
							short dir;
							do { dir = (short) rnd.nextInt(8); }while(dir == directionChoisie);
							try
							{
								nouvelleBille = testerDeplacement(neutron, (short) 1, dir);
							}
							catch(PlateauException e)
							{
								nouvelleBille = -1;
							}
						}while(nouvelleBille < 0 || cases[nouvelleBille] != eCase.VIDE);
						
						couleurNouvelleBille = choisirCouleurAuHasard((short) (nbRouges-1), nbBleues);
					}
					else if (bouchon < 0)
					{
						do
						{
							try
							{
								bouchonDistance = (short) (rnd.nextInt(4)+2);
								bouchon = testerDeplacement(neutron, bouchonDistance, directionChoisie);
							}
							catch(PlateauException e)
							{
								bouchon = -1;
							}
						}while(bouchon < 0);
						
						couleurNouvelleBille = eCase.ROUGE;
						nouvelleBille = bouchon;
					}
					else
					{
						do
						{
							nouvelleBille = (short) rnd.nextInt(25);
						}while(nouvelleBille < 0 || cases[nouvelleBille] != eCase.VIDE || obstrueDirection(neutron, nouvelleBille, directionChoisie, bouchonDistance));
						
						couleurNouvelleBille = choisirCouleurAuHasard(nbRouges, nbBleues);
					}
					
					break;
				}
			*/
				case VICTOIRE:
				//case DEFAITE:
				{
					do
					{
						nouvelleBille = (short) (rnd.nextInt(25));
					}while(cases[nouvelleBille] != eCase.VIDE || obstrueDirection(neutron, nouvelleBille, directionChoisie));
					
					couleurNouvelleBille = choisirCouleurAuHasard(nbRouges, nbBleues);
					break;
				}

			default:
				break;
			}
			
			if (couleurNouvelleBille == eCase.ROUGE)
				++nbRouges;
			else
				++nbBleues;
			
			cases[nouvelleBille] = couleurNouvelleBille;
		}
		
		return new Entry<Plateau, eCase>() {
			
			Plateau p = new Plateau(cases, eCase.ROUGE, (short) (rnd.nextInt(90)+10), null);
			eCase v = vainqueur;
			
			@Override
			public eCase setValue(eCase value) {
				// NOOP
				return v;
			}
			
			@Override
			public eCase getValue() {
				return v;
			}
			
			@Override
			public Plateau getKey() {
				return p;
			}
		};
	}
	
	private static short calculerDegreLiberte(eCase[] cases, short i_bille)
	{
		short nb_mouvements = 0;
		short[] m_mouvements = listerCoupsBille(cases, i_bille, trouverNeutron(cases));
		for(short i=0; i < 8; ++i)
		{
			if (m_mouvements[i] != i_bille) ++nb_mouvements;
		}
		
		return nb_mouvements;
	}

	private static eCase choisirCouleurAuHasard(short nbRouges, short nbBleues)
	{
		if (nbRouges < 5 && nbBleues < 5) return rnd.nextBoolean() ? eCase.ROUGE : eCase.BLEUE;
		if (nbRouges < 5) return eCase.ROUGE;
		return eCase.BLEUE;
	}
	
	private static boolean obstrueDirection(short neutron, short nouvelleBille, short directionChoisie)
	{
		return obstrueDirection(neutron, nouvelleBille, directionChoisie, Short.MAX_VALUE);
	}
	private static boolean obstrueDirection(short neutron, short nouvelleBille, short directionChoisie, short distanceMax)
	{
		for(short distance = 1; distance < distanceMax; ++distance)
		{
			try
			{
				//NOTE: testerDeplacement() doit jeter PlateauException lorsque le résultat sort du plateau (sinon infinite loop bug!).
				if (nouvelleBille == testerDeplacement(neutron, distance, directionChoisie)) return true;				// 
			}
			catch(PlateauException e)
			{
				return false;
			}
		}
		
		return false;
	}
	
	private static boolean cheminLigneDepart(short bille, short directionChoisie)
	{
		for(short distance = 1; true ; ++distance)
		{
			try
			{
				//NOTE: testerDeplacement() doit jeter PlateauException lorsque le résultat sort du plateau (sinon infinite loop bug!).
				short pos = testerDeplacement(bille, distance, directionChoisie);
				if (pos < 5 || pos >= 20) return true; 
			}
			catch(PlateauException e)
			{
				return false;
			}
			
		}
	}
	
	/**
	 * Renvoi une normalisation du plateau courant en fonction du point de vue souhaité (joueur, coté du neutron).
	 * La normalisation se contente de faire au besoin une symétrie horizontale et/ou verticale.
	 * @param joueur Point de vue à adopter (symétrie horizontale).
	 * @param neutronGauche Point de vue à adopter (symétrie verticale).
	 * @return Plateau
	 */
	public Plateau normaliser(eCase joueur, boolean neutronGauche)
	{
		if (joueur == null || (joueur != eCase.BLEUE && joueur != eCase.ROUGE)) joueur = eCase.ROUGE;
		
		if (m_Tour != joueur)
		{
			return inverserPlateau().normaliser(joueur, neutronGauche);
		}

		short[] neutron = i2coord(getNeutron());
		if ((neutronGauche && neutron[0] > 3) || (!neutronGauche && neutron[0] < 3))
		{
			// On effectue une symétrie verticale du plateau.
			eCase[] nouvelles_cases = new eCase[25];
			
			for(short j = 0; j < 2; ++j)
			{
				for(short i = 0; i < 5; ++i)
				{
					nouvelles_cases[j+i*5] = m_Cases[4-j+i*5];
					nouvelles_cases[4-j+i*5] = m_Cases[j+i*5];
				}
			}
			
			for(short i = 0; i < 5; ++i)
			{
				nouvelles_cases[2+i*5] = m_Cases[2+i*5];
			}
			
			return new Plateau(nouvelles_cases, m_Tour, m_Generation, (m_dernierCoup==null)?null:m_dernierCoup);
		}
		
		return clone();
	}
	
	
	/**
	 * Inverse joueur rouge et joueur bleu. Utile pour avoir un point de vue
	 * unique.
	 */
	public Plateau inverserPlateau()
	{
		// On effectue une flip (h&v) du plateau (en inversant les couleurs des billes).
		eCase[] nouvelles_cases = new eCase[25];
		for(short i = 0; i  < m_Cases.length; ++i)
		{
			nouvelles_cases[i] = inverserBille(m_Cases[24-i]);
		}
		
		return new Plateau(nouvelles_cases, inverserBille(m_Tour), m_Generation, (m_dernierCoup==null)?null:m_dernierCoup.inverser());
	}
	
	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString()
	{
		StringBuffer sb = new StringBuffer(35);
		
		for(short i=0; i < m_Cases.length; ++i)
		{
			switch(m_Cases[i])
			{
				case BLEUE:
					sb.append('b');
					break;
				case ROUGE:
					sb.append('r');
					break;
				case NEUTRON:
					sb.append('n');
					break;
				case VIDE:
					sb.append('.');
					break;
			}
		}
		
		sb.append(' ');
		switch(m_Tour)
		{
			case BLEUE:
				sb.append('B');
				break;
			case ROUGE:
				sb.append('R');
				break;
		}
		
		return sb.toString();
	}
	
	/**
	 * Donne l'inverse de la couleur.
	 * ROUGE -> BLEUE, BLEUE -> ROUGE, VIDE -> VIDE.
	 * @param i_bille Couleur à inverser.
	 * @return couleur inverse.
	 */
	static protected eCase inverserBille(final eCase i_bille)
	{
		if (i_bille == eCase.BLEUE) return eCase.ROUGE;
		if (i_bille == eCase.ROUGE) return eCase.BLEUE;
		return i_bille;
	}

	/**
	 * Liste les coups possible pour le joueur courant sur le plateau.
	 */
	public Set<Coup> listerCoups()
	{
		return listerCoups(m_Cases, m_bPremierCoup, m_Tour);
	}
	private static Set<Coup> listerCoups(eCase[] cases, boolean premierCoup, eCase tour)
	{
		final Set<Coup> coups = new HashSet<Coup>();
		Coup coup = null;
		final short[] coups_neutron;
		short[] coups_bille;
		boolean bDejaFait = false;
		
		short neutron = trouverNeutron(cases);
		
		if (!premierCoup)
		{
			coups_neutron = listerCoupsBille(cases, neutron, neutron);
		}
		else
		{
			coups_neutron = new short[8];
			for(short i = 0; i < 8; ++i) coups_neutron[i] = 12;
		}
		
		// Pour chaque coup possible du neutron
		for(short i = 0; i < 8; ++i)
		if ((coups_neutron[i] != neutron) || ((premierCoup) && (!bDejaFait)))
		{
			if ((coups_neutron[i] == neutron) && (premierCoup))
			{
				bDejaFait = true;
			}
			// cp.neutron_origine = neutron;
			// cp.neutron_destination = coups_neutron[i];
			
			// Pour chaque bille du joueur actuel, lister les coups possible et les noter.
			for(short bille=0; bille<25; bille++)			
			if (cases[bille] == tour)
			{
				// cp.bille_origine = bille;
				coups_bille = listerCoupsBille(cases, bille, coups_neutron[i]);
				for(short j=0; j < 8; ++j)
				if (coups_bille[j] != bille)
				{
					// cp.bille_destination = coups_bille[j];
					coup = new Coup(bille, coups_bille[j], neutron, coups_neutron[i]);
					
					if (testerPlateauTerminal(jouerCoup(cases, coup, tour), premierCoup, tour, false) != inverserBille(tour))
					{
						coups.add(coup);
					}
				}
			}
		}
		
		return coups;
	}

	/**
	 * Teste si le plateau est un plateau de fin de partie.
	 * @param bListerCoups Si VRAI, on vérifie que le joueur courant puisse jouer au moins 1 coup valide, sinon on ne vérifie que pour le plateau (direct).
	 */
	public eCase testerPlateauTerminal(boolean bListerCoups)
	{
		return testerPlateauTerminal(m_Cases, m_bPremierCoup, m_Tour, bListerCoups);
	}
	
	/**
	 * Retourne vrai si le neutron est bloqué par des billes (compté libre même sur un chemin de défaite).
	 * @param cases
	 * @return
	 */
	public boolean neutronBloque()
	{
		return neutronBloque(m_Cases);
	}
	
	private static boolean neutronBloque(eCase[] cases)
	{
		short neutron = trouverNeutron(cases);
		short[] coupsNeutron = listerCoupsBille(cases, neutron, neutron);
					
		// TODO : Essayer de remplacer les structures if/else par des affectations d'expressions boolean, on gagne en perf ?
		for(short i = 7; i >= 0; --i)
		{
			if (coupsNeutron[i] == neutron) continue;
			return false;
		}
		
		return true;
	}
	
	private static eCase testerPlateauTerminal(eCase[] cases, boolean premierCoup, eCase tour, boolean bListerCoups)
	{
		short neutron = trouverNeutron(cases);
		// Teste les cas de victoire
		
		// Le neutron est chez les rouges
		if (neutron < 5) return eCase.ROUGE;
		
		// Le neutron est chez les bleus
		if (neutron >= 20) return eCase.BLEUE;
		
		final short[] coupsNeutron = listerCoupsBille(cases, neutron, neutron);
		
		// Le neutron ne peut plus bouger
		boolean neutron_coince = true;
			
		// TODO : Essayer de remplacer les structures if/else par des affectations d'expressions boolean, on gagne en perf ?
		for(short i = 7; i >= 0; --i)
		{
			if (coupsNeutron[i] == neutron) continue;
			
			// On vérifie la destination
			
			// C'est le tour de rouge, et il peut jouer le neutron dans son camp.
			if ((tour == eCase.ROUGE) && (coupsNeutron[i] < 5))
			{
				return eCase.ROUGE;
			}
			
			// C'est le tour de bleu, et il peut jouer le neutron dans son camp.
			else if ((tour == eCase.BLEUE) && (coupsNeutron[i] >= 20))
			{
				return eCase.BLEUE;
			}
			
			if (neutron_coince)
			{
				// On décoince le joueur s'il peut être joué ailleurs que chez l'adversaire.
				if (tour == eCase.ROUGE)
				{
					if (coupsNeutron[i] < 20) neutron_coince = false;
				}
				else
				{
					if (coupsNeutron[i] >= 5) neutron_coince = false;
				}
			}
		}
		
		if (neutron_coince)
		{
			return inverserBille(tour);
		}
		
		if (bListerCoups)
		{
			Set<Coup> coups = listerCoups(cases, premierCoup, tour);
			if (coups.size() == 0)
			{
				return inverserBille(tour);
			}
			
		}
		
		return eCase.VIDE;
	}
	
	public eCase getTour()
	{
		return m_Tour;
	}
	
	public eCase[] getCases()
	{
		return m_Cases.clone();
	}
	
	/**
	 * Retourne la position du neutron (0-24).
	 */
	public short getNeutron()
	{
		return trouverNeutron(m_Cases);
	}
	
	/**
	 * Indique si le plateau est un plateau inital.
	 */
	public boolean estUnPlateauInitial()
	{
		return m_bPremierCoup;
	}
	
	/**
	 * Retoure la génération du plateau (0 est réservé au plateau initial).
	 */
	public short getGeneration()
	{
		return m_Generation;
	}
	
	/* (non-Javadoc)
	 * @see java.lang.Object#clone()
	 */
	@Override
	public Plateau clone()
	{
		return new Plateau(this);
	}
	
	// Métriques
	/** Vrai si le neutron n'a qu'un mouvement possible. */
	private Boolean isNeutronForce = null;
	
	public boolean isNeutronForce()
	{
		if (isNeutronForce == null)
		{
			computeMetrics();
		}
		return isNeutronForce;
	}
	
	/** Nombre de billes mobiles. */
	private Short nbBillesMobiles = null;
	private Short nbBillesMobilesAdv = null;
	
	public short getNbBillesMobiles()
	{
		if (nbBillesMobiles == null)
		{
			computeMetrics();
		}
		return nbBillesMobiles;
	}
	
	public short getNbBillesMobilesAdv()
	{
		if (nbBillesMobilesAdv == null)
		{
			computeMetrics();
		}
		return nbBillesMobilesAdv;
	}
	
	/** Nombre total de directions libres (toutes les billes). */
	private Integer nbDirectionsPossibles = null;
	private Integer nbDirectionsPossiblesAdv = null;
	
	public short getNbDirectionsPossibles()
	{
		if (nbDirectionsPossibles == null)
		{
			computeMetrics();
		}
		return nbDirectionsPossibles.shortValue();
	}
	
	public short getNbDirectionsPossiblesAdv()
	{
		if (nbDirectionsPossiblesAdv == null)
		{
			computeMetrics();
		}
		return nbDirectionsPossiblesAdv.shortValue();
	}
	
	/** Nombre de cases victoire libres. */
	private Short nbCasesVictoiresLibres = null;
	private Short nbCasesDefaitesLibres = null;
	
	public short getNbCasesVictoiresLibres()
	{
		if (nbCasesVictoiresLibres == null)
		{
			computeMetrics();
		}
		return nbCasesVictoiresLibres;
	}
	
	public short getNbCasesDefaitesLibres()
	{
		if (nbCasesDefaitesLibres == null)
		{
			computeMetrics();
		}
		return nbCasesDefaitesLibres;
	}
	
	synchronized private void computeMetrics()
	{
		nbBillesMobiles = 0;
		nbBillesMobilesAdv = 0;
		nbDirectionsPossibles = 0;
		nbDirectionsPossiblesAdv = 0;
		nbCasesVictoiresLibres = 0;
		nbCasesDefaitesLibres = 0;		
		
		for(short i=0; i<25; ++i)
		{
			if (m_Cases[i] != eCase.VIDE)
			{
				short[] coups = listerCoupsBille(m_Cases, i, getNeutron());
				Short billeDirectionsPossibles = 0;
				for(short d=0; d<8; ++d)
				{
					if (coups[d] != i) ++billeDirectionsPossibles;
				}
				
				if (m_Cases[i] == m_Tour)
				{
					if (billeDirectionsPossibles > 0) ++nbBillesMobiles;
					nbDirectionsPossibles += billeDirectionsPossibles;
				}
				else if (m_Cases[i] == eCase.NEUTRON)
				{
					isNeutronForce = (billeDirectionsPossibles <= 0);
				}
				else // Opponent
				{
					if (billeDirectionsPossibles > 0) ++nbBillesMobilesAdv;
					nbDirectionsPossiblesAdv += billeDirectionsPossibles;
				}
			}
			else
			{
				if (i < 5)
				{
					if (m_Tour == eCase.ROUGE) ++nbCasesVictoiresLibres;
					else if (m_Tour == eCase.BLEUE) ++nbCasesDefaitesLibres;
				}
				else if (i >= 20)
				{
					if (m_Tour == eCase.BLEUE) ++nbCasesVictoiresLibres;
					else if (m_Tour == eCase.ROUGE) ++nbCasesDefaitesLibres;
				}
			}
		}
		/*
		POUR CHAQUE case DU plateau FAIRE
			SI case != vide ALORS
				coupsPossibles <- listeCoupsBille(case)
				billeDir = NbDirectionsPossibles(coups)
				SI case = tour ALORS
					SI billeDir > 0 ALORS ++nbBillesMobiles
					nbDirectionsPossibles += billeDir					
				SINON SI case = Neutron ALORS					
					isNeutronForce = (billeDir > 0)
				SINON
					SI billeDir > 0 ALORS ++nbBillesMobilesAdv
					nbDirectionsPossiblesAdv += billeBir
				FSI
			SINON
				SI EstCaseVictoire(case) ALORS ++nbCasesVictoiresLibres
				SI EstCaseDefaite(case) ALORS ++nbCasesDefaitesLibres
			FSI
		FPOUR
		 */
	}
	
	/*
	Le tout dans une seule fonction qui analyse le plateau et peuple tout les résultats.
	
	/**
	 * Vrai si le neutron n'a qu'un mouvement possible.
	 *
	boolean isNeutronForce()
	
	/**
	 * Nombre de billes mobiles  
	 *
	int getNbBillesMobiles()
	int getNbBillesMobilesAdv()
	
	/**
	 * Nombre total de directions libres (toutes les billes)
	int getNbDirectionsPossibles()
	int getNbDirectionsPossiblesAdv()
	
	/**
	 * Nombre de cases victoire libres
	int getNbCasesVictoiresLibres()
	
	/**
	 * Nombre de cases défaite libres
	int getNbCasesDefaitesLibres()
	
	
	 
	 */
}
